using Microsoft.Practices.EnterpriseLibrary.ExceptionHandling;
using Sandia.MVCFramework.Data;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace Sandia.MVCFramework.Common
{
    /// <summary>
    /// Implementation of IPublisher that provides
    /// standard functionality for keeping track of subscribers
    /// and publishing data to them
    /// </summary>
    public class StandardPublisher: IPublisher
    {
        /// <summary>
        /// Used to synchronize access to this.dataSubscriptions. 
        /// </summary>
        /// <remarks>
        /// Allows multiple threads to read, or one thread to write at any given
        /// time. For more information, see the following URL:
        /// 
        /// http://msdn2.microsoft.com/en-us/library/system.threading.readerwriterlock.aspx
        /// 
        /// This object was added in part to address Bug #969.
        /// </remarks>
        protected ReaderWriterLock subscriptionsReaderWriterLock;

        /// <summary>
        /// Used to keep track of subscribers; note that the internal dictionary
        /// is provided for O(1) lookups of subscribers, thus the "bool" typed value
        /// is not used and is a placeholder.
        /// </summary>
        protected Dictionary<Type, Dictionary<ISubscriber, bool>> dataSubscriptions;

        /// <summary>
        /// Used to keep track of subscribers; note that the internal dictionary
        /// is provided for O(1) lookups of subscribers, thus the "bool" typed value
        /// is not used and is a placeholder.
        /// </summary>
        public Dictionary<Type, Dictionary<ISubscriber, bool>> DataSubscriptions
        {
            get
            {
                return this.dataSubscriptions;
            }
        }

        /// <summary>
        /// Default constructor; initializes tracking of subscribers
        /// </summary>
        public StandardPublisher()
        {
            this.dataSubscriptions = new Dictionary<Type, Dictionary<ISubscriber, Boolean>>();
            this.subscriptionsReaderWriterLock = new ReaderWriterLock();
        }

        private void publishToType(DataPacket dataPacket, Type type)
        {
            // Aquire a reader lock for this.dataSubscriptions.
            this.subscriptionsReaderWriterLock.AcquireReaderLock(Timeout.Infinite);
            try
            {
                // Check to see if we have any subscribers for this data type
                if (this.dataSubscriptions.ContainsKey(type))
                {
                    // Get the subscribers to this data type, and
                    // tell each one to handle the published data
                    Dictionary<ISubscriber, bool> subscribers = this.dataSubscriptions[type];
                    foreach (KeyValuePair<ISubscriber, bool> keyValuePair in subscribers)
                    {
                        keyValuePair.Key.HandlePublishedData(dataPacket);

                        // Generate a log entry
                        LoggingTools.SendToLog(
                            "\nData of type " + dataPacket.DataType +
                            "\npublished to " + keyValuePair.Key.GetType().ToString() +
                            "\nbased on subscription to type " + type.ToString() +
                            "\nby " + this.GetType().ToString() +
                            "\nData: " + dataPacket.Data.ToString(),
                            LoggingCategories.DATA_PUBLISH,
                            0);
                    }
                }
            }
            catch (Exception e)
            {
                // Use exception handling policy to determine what to do
                bool rethrow = ExceptionPolicy.HandleException(e, "Default Policy");
                if (rethrow)
                {
                    throw;
                }
            }
            finally
            {
                // Ensure that the lock is released.
                this.subscriptionsReaderWriterLock.ReleaseReaderLock();
            }
        }

        /// <summary>
        /// Publishes a data packet out to subscribers that are subscribed
        /// to the data type carried by the packet
        /// </summary>
        /// <param name="dataPacket">The data packet to be published</param>
        public virtual void PublishData(DataPacket dataPacket)
        {
            // Check for null.
            if (dataPacket == null)
            {
                LoggingTools.SendToLog(
                    "PublishData: dataPacket is null.",
                    LoggingCategories.ERROR,
                    100);
            }

            if (dataPacket.PacketID == null)
            {
                LoggingTools.SendToLog(
                    "PublishData: dataPacket.PacketID is null.",
                    LoggingCategories.ERROR,
                    100);
            }

            if (dataPacket.DataType == null)
            {
                LoggingTools.SendToLog(
                    "PublishData: dataPacket.DataType is null.",
                    LoggingCategories.ERROR,
                    100);
            }

            if (dataPacket.Data == null)
            {
                LoggingTools.SendToLog(
                    "PublishData: dataPacket.Data is null.",
                    LoggingCategories.ERROR,
                    100);
            }

            // Try publishing to the type itself
            this.publishToType(dataPacket, dataPacket.DataType);

            // Iterate over all interfaces the type implements, and
            // see if anyone subscribes to those
            foreach (Type interfaceType in dataPacket.DataType.GetInterfaces())
            {
                this.publishToType(dataPacket, interfaceType);
            }
            
            // Walk up inheritance tree (all the way up to Object) to see
            // if anyone is subscribing to more generic parent types
            Type parentType = dataPacket.DataType.BaseType;
            while (parentType != null)
            {                
                // See if anyone subscribes to this parent type
                this.publishToType(dataPacket, parentType);

                // Move up inheritance tree
                parentType = parentType.BaseType;
            }
        }

        #region IPublisher Members

        /// <summary>
        /// Handle a requestion for a subscription
        /// from a specified subscriber for a
        /// specified data type
        /// </summary>
        /// <param name="subscriber">The subscriber
        /// making the subscription request</param>
        /// <param name="dataType">The data type
        /// to subscribe to</param>
        public void Subscribe(ISubscriber subscriber, Type dataType)
        {
            // Acquire a writer lock for this.dataSubscriptions. Writer locks
            // are queued and executed exclusive of other operations.
            this.subscriptionsReaderWriterLock.AcquireWriterLock(Timeout.Infinite);
            try
            {
                // JTM 20080417 - Refactored this code a little to make it
                // fewer lines.
                if (!this.dataSubscriptions.ContainsKey(dataType))
                {
                    // No subscribers to this data type yet, so
                    // set one up
                    this.dataSubscriptions[dataType] =
                        new Dictionary<ISubscriber, bool>();
                }
                // Add this subscriber to the collection
                // of existing subscribers for this data type
                if (!this.dataSubscriptions[dataType].ContainsKey(subscriber))
                {
                    subscriber.Disposed += new EventHandler(subscriber_Disposed);
                    this.dataSubscriptions[dataType][subscriber] = true;
                }
            }
            finally
            {
                // Ensure that the lock is released.
                this.subscriptionsReaderWriterLock.ReleaseWriterLock();
            }
        }

        /// <summary>
        /// Remove subscription to the specified data type for the given
        /// subscriber.
        /// </summary>
        /// <param name="subscriber">The subscriber
        /// to remove</param>
        /// <param name="dataType">The data type
        /// to remove from</param>
        public void RemoveSubscription(
            ISubscriber subscriber,
            Type dataType)
        {
            // Acquire a writer lock for this.dataSubscriptions. Writer locks
            // are queued and executed exclusive of other operations.
            this.subscriptionsReaderWriterLock.AcquireWriterLock(Timeout.Infinite);
            try
            {
                if (this.dataSubscriptions.ContainsKey(dataType)
                    && this.dataSubscriptions[dataType].ContainsKey(subscriber))
                {
                    this.dataSubscriptions[dataType].Remove(subscriber);
                }
            }
            finally
            {
                // Ensure that the lock is released.
                this.subscriptionsReaderWriterLock.ReleaseWriterLock();
            }
        }

        /// <summary>
        /// After a subscriber is disposed, it has to be removed from
        /// the routing list.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void subscriber_Disposed(object sender, EventArgs e)
        {
            ISubscriber subscriber = sender as ISubscriber;
            if (subscriber == null) return;

            // Acquire a writer lock for this.dataSubscriptions. Writer locks
            // are queued and executed exclusive of other operations.
            this.subscriptionsReaderWriterLock.AcquireWriterLock(Timeout.Infinite);
            try
            {
                foreach (Dictionary<ISubscriber, bool> subscriberList in dataSubscriptions.Values)
                {
                    if (subscriberList.ContainsKey(subscriber))
                        subscriberList.Remove(subscriber);
                }
            }
            finally
            {
                // Ensure that the lock is released.
                this.subscriptionsReaderWriterLock.ReleaseWriterLock();
            }
        }

        #endregion
    }
}
